﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using NHibernate.Mappings;

namespace Apollo.Core
{
    public class Projection
    {
        protected string projectionString;
        protected Type baseType;
        protected PropertyInfo basePropertyInfo;
        protected EntityProperty entityProperty;
        protected Entity relationEntity;


        /// <summary>
        /// The alias used for this projection
        /// </summary>
        public string Alias { get; set; }

        /// <summary>
        /// The calculated display name, according to the language
        /// </summary>
        public string DisplayName
        {
            get
            {
                if (!HasInnerProjection)
                {
                    LanguageDefinition ld = Apollo.Application.Meta<LanguageDefinition>()
                        .SingleOrDefault<LanguageDefinition>(l =>
                            l.Type == "EntityProperty"
                            && l.CodeHi == this.baseType.Name
                            && l.CodeLo == this.PropertyName
                            && l.Language.Id == Apollo.Application.CurrentLanguage.Id);

                    if (ld != null)
                        return ld.DisplayName;
                    else
                        return this.PropertyName;
                }
                else
                {
                    LanguageDefinition ld = Apollo.Application.Meta<LanguageDefinition>()
                        .SingleOrDefault<LanguageDefinition>(l =>
                            l.Type == "EntityProjection"
                            && l.CodeHi == baseType.Name
                            && l.CodeLo == this.ProjectionPath
                            && l.Language.Id == Apollo.Application.CurrentLanguage.Id);

                    if (ld != null)
                        return ld.DisplayName;
                    else
                    {
                        ld = Apollo.Application.Meta<LanguageDefinition>()
                            .SingleOrDefault<LanguageDefinition>(l =>
                                l.Type == "EntityProperty"
                                && l.CodeHi == this.baseType.Name
                                && l.CodeLo == this.basePropertyInfo.Name
                                && l.Language.Id == Apollo.Application.CurrentLanguage.Id);

                        if (ld != null)
                            return ld.DisplayName + " > " + this.InnerProjection.DisplayName;
                        else
                            return this.basePropertyInfo.Name + " > " + this.InnerProjection.DisplayName;
                    }
                }
            }
        }

        public bool IsValid { get; private set; }
        
        public Projection InnerProjection { get; private set; }

        public Type BaseType { get { return this.baseType; } }

        /// <summary>
        /// The actual containing type of the refered property. The type of the last class in the projection.
        /// </summary>
        public Type ClassType
        {
            get
            {
                if (!this.HasInnerProjection)
                {
                    if (this.IsRelation)
                        return this.PropertyType;
                    else
                        return this.baseType;
                }
                else
                    return this.InnerProjection.ClassType;
            }
        }
        
        /// <summary>
        /// The actual name of the refered property. The name of the last property in the projection.
        /// </summary>
        public string PropertyName
        {
            get
            {
                if (!this.HasInnerProjection)
                    return this.projectionString;
                else
                    return this.InnerProjection.PropertyName;
            }
        }

        /// <summary>
        /// The actual type of the refered property. The type of the last class in the projection.
        /// </summary>
        public Type PropertyType { get {
            if (this.HasInnerProjection)
                return this.InnerProjection.PropertyType;
            else if (this.basePropertyInfo != null)
                return this.basePropertyInfo.PropertyType;
            else
                return null;
        } }

        public string PropertyTypeForJSON { get {
            if (this.PropertyType == typeof(int) || this.PropertyType == typeof(int?))
                return "int";
            else if (this.PropertyType == typeof(DateTime) || this.PropertyType == typeof(DateTime?))
                return "datetime";
            else if (this.PropertyType == typeof(decimal) || this.PropertyType == typeof(float) || this.PropertyType == typeof(double) || this.PropertyType == typeof(decimal?) || this.PropertyType == typeof(float?) || this.PropertyType == typeof(double?))
                return "double";
            else if (this.PropertyType == typeof(bool) || this.PropertyType == typeof(bool?))
                return "bool";
            else if (this.PropertyType == typeof(string))
                return "string";
            else if (this.PropertyType.IsEnum)
                return "enum";
            else
                return "object";
        } }

        public bool IsRelation { get {
            object[] attrs = this.PropertyType.GetCustomAttributes(typeof(NHibernateEntityAttribute), true);
            if(attrs == null || attrs.Length <= 0)
                return false;

            return true;
        } }

        public bool IsReadOnly { get {
            if (this.basePropertyInfo == null)
                return true;

            bool isReadOnly = false;

            object[] attrs = this.basePropertyInfo.GetCustomAttributes(typeof(BelongsToAttribute), true);
            if (attrs != null && attrs.Length > 0)
                isReadOnly |= attrs.ToList().Exists(a => ((BelongsToAttribute)a).Access == PropertyAccess.ReadOnly);

            attrs = this.basePropertyInfo.GetCustomAttributes(typeof(PropertyAttribute), true);
            if (attrs != null && attrs.Length > 0)
                isReadOnly |= attrs.ToList().Exists(a => ((PropertyAttribute)a).Access == PropertyAccess.ReadOnly);

            attrs = this.basePropertyInfo.GetCustomAttributes(typeof(ReadOnlyAttribute), true);
            if (attrs != null && attrs.Length > 0)
                isReadOnly = true;

            return isReadOnly;
        } }
        
        public bool HasInnerProjection { get { return InnerProjection != null; } }

        /// <summary>
        /// The path of the projection as first given
        /// </summary>
        public string ProjectionPath { get { return projectionString;  } }

        /// <summary>
        /// The path of the projection without the final property
        /// </summary>
        public string ProjectionPathWithoutProperty { get { 
            if(this.HasInnerProjection) {
                string innerPath = this.InnerProjection.ProjectionPathWithoutProperty;

                if (string.IsNullOrEmpty(innerPath))
                    return this.basePropertyInfo.Name;
                else
                    return this.basePropertyInfo.Name + "." + innerPath;
            } else {
                if (this.IsRelation)
                    return this.basePropertyInfo.Name;
                else
                    return null;
            }
        } }

        public EntityProperty EntityProperty { get {
            if (this.HasInnerProjection)
                return this.InnerProjection.EntityProperty;
            else if (entityProperty == null) {
                entityProperty = Apollo.Application.Meta<EntityProperty>()
                            .SingleOrDefault<EntityProperty>(ep =>
                                ep.Entity.Name == baseType.Name && ep.PropertyName == this.PropertyName);
            }

            return entityProperty; 
        } }

        public Projection(string projection, Type baseType) {
            this.IsValid = true;

            this.projectionString = projection;
            this.baseType = baseType;

            if (projection.Contains("."))
            {
                int indexOfDot = this.projectionString.IndexOf(".");

                string basePropertyName = this.projectionString.Substring(0, indexOfDot);
                try
                {
                    this.basePropertyInfo = this.baseType.GetProperty(basePropertyName);

                    if (this.basePropertyInfo == null) {
                        this.IsValid = false;
                        return;
                    }

                    this.InnerProjection = new Projection(
                        projection.Substring(indexOfDot + 1),
                        this.basePropertyInfo.PropertyType
                    );

                    this.IsValid = InnerProjection.IsValid;
                }
                catch
                {
                    this.IsValid = false;
                }
            }
            else {
                try
                {
                    this.basePropertyInfo = this.baseType.GetProperty(this.projectionString);
                    if (this.basePropertyInfo == null)
                    {
                        this.IsValid = false;
                        return;
                    }
                }
                catch {
                    this.IsValid = false;
                }
            }
        }

        public object Apply(object obj){
            if (!this.IsValid)
                throw new Exception("Can not apply an invalid projection '" 
                    + this.projectionString 
                    +"' for entity '" + this.baseType.Name + "'");

            if (obj == null)
                return null;

            object value = this.basePropertyInfo.GetValue(obj, null);

            if (this.InnerProjection != null)
                value = InnerProjection.Apply(value);

            return value;
        }

        public void SetValue(object obj, object value) {
            if (!this.IsValid)
                throw new Exception("Can not apply an invalid projection");

            if (obj == null)
                return;

            if (this.InnerProjection == null)
            {
                this.basePropertyInfo.SetValue(obj, value, null);
            }
            else { 
                // TODO
            }
        }

        
        public Entity RelationEntity { get {
            if (!this.IsRelation)
                return null;

            if(relationEntity == null) {
                relationEntity = Apollo.Application.Meta<Entity>()
                    .SingleOrDefault<Entity>(e => e.Name == this.ClassType.Name);
            }

            return relationEntity;
        } }

        public string ToString(object obj, bool generateHTMLCode) {
            object actualObject = this.Apply(obj);

            if (this.IsRelation)
            {
                if (actualObject == null || this.RelationEntity == null)
                    return string.Empty;

                Projection relationIdentityProjection = new Projection(this.RelationEntity.IdentifierProperty, this.RelationEntity.MappedClass);
                Projection relationDisplayProjection = new Projection(this.RelationEntity.DisplayProperty, this.RelationEntity.MappedClass);

                string idValue = relationIdentityProjection.ToString(actualObject, false);
                string displayValue = relationDisplayProjection.ToString(actualObject, false);

                if (string.IsNullOrEmpty(displayValue))
                    displayValue = "{" + idValue + "}";

                if (generateHTMLCode)
                {
                    string editUrl = string.IsNullOrEmpty(this.RelationEntity.CustomEditUrl) ?
                                    "Edit.aspx?EntityId=" + this.RelationEntity.Id.ToString() + "&ObjectId={ObjectId}" :
                                    this.RelationEntity.CustomEditUrl;
                    editUrl = editUrl.Replace("{ObjectId}", idValue);

                    return string.Format("<a href='javascript:ModalOpen(\"{0}\", false)'>{1}</a>", editUrl, displayValue);
                }
                else
                    return displayValue;
            }
            else
            {
                if(string.IsNullOrEmpty(this.EntityProperty.FormatString))
                    return Apollo.Application.Serialize(actualObject, true);
                else
                    return Apollo.Application.Serialize(actualObject, this.EntityProperty.FormatString);
            }
        }
    }
}
